/*
 * Copyright 2016 Miroslav Janíček
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.sandius.rembulan.compiler;

import java.util.Objects;

/**
 * An immutable class encapsulating the settings of the compilation of Lua to Java bytecode
 * by {@link LuaCompiler}.
 *
 * <p>The settings are the following</p>
 * <ul>
 *     <li><b>CPU accounting</b> ({@link CPUAccountingMode}): controls whether and how
 *       the functions generated by the compiler account for number of ticks spent in execution
 *       and pause when their time slice has expired;</li>
 *     <li><b>const folding</b> (boolean): when {@code true}, constants are folded at compile
 *       time (note that this does not have an influence on the number of ticks counted);</li>
 *     <li><b>const caching</b> (boolean): when {@code true}, boxed numeric constants are stored
 *       as static fields rather than being instantiated (and boxed) at execution time;</li>
 *     <li><b>node size limit</b> (int): when positive, long functions are split up into smaller
 *       Java methods (each containing at most the specified number of IR nodes); otherwise,
 *       a single method containing the entire function code is generated. Java class files
 *       impose a strict limit of 64 kB per method: this setting allows the compilation
 *       of arbitrarily-long Lua functions.</li>
 * </ul>
 *
 * <p>To obtain the settings with sensible defaults, use {@link CompilerSettings#defaultSettings()}.
 * To obtain the default settings with CPU accounting disabled,
 * use {@link CompilerSettings#defaultNoAccountingSettings()}.
 * </p>
 */
public final class CompilerSettings {

	/**
	 * CPU accounting mode.
	 */
	public enum CPUAccountingMode {

		/**
		 * Do not do CPU accounting.
		 */
		NO_CPU_ACCOUNTING,

		/**
		 * Check CPU time usage at the beginning of every basic block.
		 *
		 * <p>At the beginning of every basic block, update the ticks by invoking
		 * {@link net.sandius.rembulan.runtime.ExecutionContext#registerTicks(int)}
		 * and potentially pause by invoking
		 * {@link net.sandius.rembulan.runtime.ExecutionContext#pauseIfRequested()}.</p>
		 */
		IN_EVERY_BASIC_BLOCK

	}

	/**
	 * The default CPU accounting mode.
	 */
	public static final CPUAccountingMode DEFAULT_CPU_ACCOUNTING_MODE = CPUAccountingMode.IN_EVERY_BASIC_BLOCK;

	/**
	 * The default const folding mode.
	 */
	public static final boolean DEFAULT_CONST_FOLDING_MODE = true;

	/**
	 * The default const caching mode.
	 */
	public static final boolean DEFAULT_CONST_CACHING_MODE = true;

	/**
	 * The default byte string mode.
	 */
	public static final boolean DEFAULT_BYTE_STRING_MODE = true;

	/**
	 * The default method size limit.
	 */
	public static final int DEFAULT_NODE_SIZE_LIMIT = 2000;

	private final CPUAccountingMode cpuAccountingMode;
	private final boolean constFolding;
	private final boolean constCaching;
	private final boolean byteStrings;
	private final int nodeSizeLimit;

	CompilerSettings(
			CPUAccountingMode cpuAccountingMode,
			boolean constFolding,
			boolean constCaching,
			boolean byteStrings,
			int nodeSizeLimit) {

		this.cpuAccountingMode = Objects.requireNonNull(cpuAccountingMode);
		this.constFolding = constFolding;
		this.constCaching = constCaching;
		this.byteStrings = byteStrings;
		this.nodeSizeLimit = nodeSizeLimit;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		CompilerSettings that = (CompilerSettings) o;

		return this.cpuAccountingMode == that.cpuAccountingMode
				&& this.constFolding == that.constFolding
				&& this.constCaching == that.constCaching
				&& this.byteStrings == that.byteStrings
				&& this.nodeSizeLimit == that.nodeSizeLimit;
	}

	@Override
	public int hashCode() {
		int result = cpuAccountingMode.hashCode();
		result = 31 * result + (constFolding ? 1 : 0);
		result = 31 * result + (constCaching ? 1 : 0);
		result = 31 * result + (byteStrings ? 1 : 0);
		result = 31 * result + nodeSizeLimit;
		return result;
	}

	/**
	 * Returns the compiler settings with the given parameters.
	 *
	 * <p>When {@code nodeSizeLimit} is non-positive, no chunking of the body method
	 * will be performed.</p>
	 *
	 * @param cpuAccountingMode  CPU accounting mode, must not be {@code null}
	 * @param constFolding  const folding mode
	 * @param constCaching  const caching mode
	 * @param byteStrings  byte string mode
	 * @param nodeSizeLimit  node size limit
	 * @return  the corresponding compiler settings
	 *
	 * @throws NullPointerException  if {@code cpuAccountingMode} is {@code null}
	 */
	public static CompilerSettings of(
			CPUAccountingMode cpuAccountingMode,
			boolean constFolding,
			boolean constCaching,
			boolean byteStrings,
			int nodeSizeLimit) {

		return new CompilerSettings(
				cpuAccountingMode, constFolding, constCaching, byteStrings, nodeSizeLimit);
	}

	/**
	 * Returns the default compiler settings.
	 *
	 * @return  the default compiler settings
	 */
	public static CompilerSettings defaultSettings() {
		return CompilerSettings.of(
				DEFAULT_CPU_ACCOUNTING_MODE,
				DEFAULT_CONST_FOLDING_MODE,
				DEFAULT_CONST_CACHING_MODE,
				DEFAULT_BYTE_STRING_MODE,
				DEFAULT_NODE_SIZE_LIMIT);
	}

	/**
	 * Returns the default compiler settings without CPU accounting.
	 *
	 * @return  the default compiler settings without CPU accounting
	 */
	public static CompilerSettings defaultNoAccountingSettings() {
		return defaultSettings().withCPUAccountingMode(CPUAccountingMode.NO_CPU_ACCOUNTING);
	}

	/**
	 * Returns the CPU accounting mode.
	 *
	 * @return  the CPU accounting mode
	 */
	public CPUAccountingMode cpuAccountingMode() {
		return cpuAccountingMode;
	}

	/**
	 * Returns the const folding mode.
	 *
	 * @return  the const folding mode
	 */
	public boolean constFolding() {
		return constFolding;
	}

	/**
	 * Returns the const caching mode.
	 *
	 * @return  the const caching mode
	 */
	public boolean constCaching() {
		return constCaching;
	}

	public boolean byteStrings() {
		return byteStrings;
	}

	/**
	 * Returns the node size limit.
	 *
	 * @return  the node size limit
	 */
	public int nodeSizeLimit() {
		return nodeSizeLimit;
	}

	/**
	 * Returns compiler settings derived from this compiler settings by updating
	 * the CPU accounting mode to {@code mode}.
	 *
	 * @param mode  new CPU accounting mode, must not be {@code null}
	 * @return  settings derived from {@code this} by updating the CPU accounting mode
	 *          to {@code mode}
	 *
	 * @throws NullPointerException  if {@code mode} is {@code null}
	 */
	public CompilerSettings withCPUAccountingMode(CPUAccountingMode mode) {
		return mode != this.cpuAccountingMode
				? new CompilerSettings(mode, constFolding, constCaching, byteStrings, nodeSizeLimit)
				: this;
	}

	/**
	 * Returns compiler settings derived from this compiler settings by updating
	 * the const folding mode to {@code mode}.
	 *
	 * @param mode  new const folding mode
	 * @return  settings derived from {@code this} by updating the const folding mode
	 *          to {@code mode}
	 */
	public CompilerSettings withConstFolding(boolean mode) {
		return mode != this.constFolding
				? new CompilerSettings(cpuAccountingMode, mode, constCaching, byteStrings, nodeSizeLimit)
				: this;
	}

	/**
	 * Returns compiler settings derived from this compiler settings by updating
	 * the const caching mode to {@code mode}.
	 *
	 * @param mode  new const caching mode
	 * @return  settings derived from {@code this} by updating the const caching mode
	 *          to {@code mode}
	 */
	public CompilerSettings withConstCaching(boolean mode) {
		return mode != this.constCaching
				? new CompilerSettings(cpuAccountingMode, constFolding, mode, byteStrings, nodeSizeLimit)
				: this;
	}

	/**
	 * Returns compiler settings derived from this compiler settings by updating
	 * the byte string mode to {@code mode}.
	 *
	 * @param mode  new byte string mode
	 * @return  settings derived from {@code this} by updating the byte string mode to {@code mode}
	 */
	public CompilerSettings withByteStrings(boolean mode) {
		return mode != this.byteStrings
				? new CompilerSettings(cpuAccountingMode, constFolding, constCaching, mode, nodeSizeLimit)
				: this;
	}

	/**
	 * Returns compiler settings derived from this compiler settings by updating
	 * the node size limit to {@code limit}.
	 *
	 * @param limit  new node size limit
	 * @return  settings derived from {@code this} by updating the node size limit
	 *          to {@code limit}
	 */
	public CompilerSettings withNodeSizeLimit(int limit) {
		return limit != this.nodeSizeLimit
				? new CompilerSettings(cpuAccountingMode, constFolding, constCaching, byteStrings, limit)
				: this;
	}

}
